using System.Text;
using Newtonsoft.Json.Linq;

namespace Json2Class;

public class CSharpJson2Class
{
    private readonly StringBuilder _sb;
    private int _indentLevel;
    private readonly HashSet<string> _generatedClasses;
    private string _rootClassName;

    public CSharpJson2Class(string rootClassName = "RootObject")
    {
        this._rootClassName = rootClassName;
        _sb = new StringBuilder();
        _generatedClasses = new HashSet<string>();
        _indentLevel = 0;
    }

    public bool TryGenerate(string jsonString, out string codeString)
    {
        try
        {
            var token = JToken.Parse(jsonString);
            GenerateClass(this._rootClassName, token);
            codeString = _sb.ToString();
            return true;
        }
        catch (Exception ex)
        {
            codeString = $"Error parsing JSON: {ex.Message}";
            return false;
        }
    }
    private void GenerateClass(string className, JToken token)
    {
        if (_generatedClasses.Contains(className))
            return;

        _generatedClasses.Add(className);

        _sb.AppendLine();
        _sb.AppendLine($"public class {className}");
        _sb.AppendLine("{");
        _indentLevel++;

        if (token is JObject obj)
        {
            foreach (var property in obj.Properties())
            {
                var propName = ToPascalCase(property.Name);
                var propType = GetPropertyType(property.Value);
                if(propName!=property.Name)
                {                    
                    WriteIndent();
                    _sb.AppendLine($"[JsonProperty(\"{property.Name}\")]");
                }
                WriteIndent();
                _sb.AppendLine($"public {propType} {propName} {{ get; set; }}");
            }
        }

        _indentLevel--;
        _sb.AppendLine("}");

        if (token is JObject jObject)
        {
            foreach (var property in jObject.Properties())
            {
                if (property.Value is JObject childObj)
                {
                    var childClassName = ToPascalCase(property.Name);
                    GenerateClass(childClassName, childObj);
                }
                else if (property.Value is JArray array && array.Count > 0)
                {
                    var firstItem = array.First;
                    if (firstItem is JObject arrayObj)
                    {
                        var childClassName = ToPascalCase(property.Name) + "Item";
                        GenerateClass(childClassName, arrayObj);
                    }
                }
            }
        }
    }
    private string GetPropertyType(JToken token)
    {
        switch (token.Type)
        {
            case JTokenType.Object:
                return ToPascalCase(token.Path);
            case JTokenType.Array:
                var arrayType = GetArrayType(token as JArray);
                return $"List<{arrayType}>";
            case JTokenType.Integer:
                return token.Value<long>() > int.MaxValue ? "long" : "int";
            case JTokenType.Float:
                return "double";
            case JTokenType.Boolean:
                return "bool";
            case JTokenType.Null:
            case JTokenType.String:
            default:
                return "string";
        }
    }
    private string GetArrayType(JArray array)
    {
        if (array.Count == 0) 
            return "object";
        
        var firstItem = array.First;
        if (firstItem.Type == JTokenType.Object)
        {
            return ToPascalCase(firstItem.Path) + "Item";
        }
        
        return GetPropertyType(firstItem);
    }
    private string ToPascalCase(string input)
    {
        if (string.IsNullOrEmpty(input)) 
            return "NullName";
        var subStrs = input.Split('_');
        if (subStrs.Length > 1)
        {
            if(double.TryParse(subStrs[0].Substring(0,1), out _))
                subStrs[0] = "The" + subStrs[0];
            return string.Join("", subStrs.Select(c => char.ToUpper(c[0]) + c.Substring(1)));       
        }
        return char.ToUpper(input[0]) + input.Substring(1);
    }
    private void WriteIndent()
    {
        _sb.Append(' ', _indentLevel * 4);
    }
}